#region Using References

using System;
using System.Diagnostics.Contracts;
using System.IO;

#endregion

namespace Beaker.OpenCube.IO
{
	public sealed class BigEndianBinaryReader
	{
		private readonly Stream _stream;
		private readonly byte[] _buffer;

		public BigEndianBinaryReader( Stream stream )
		{
			Contract.Requires( stream != null );
			Contract.Ensures( _stream != null );
			Contract.Ensures( _stream == stream );
			Contract.Ensures( _buffer != null );
			Contract.Ensures( _buffer.Length == 16 );

			_buffer = new byte[16];
			_stream = stream;
		}

		[ContractInvariantMethod]
		private void ContractInvariant()
		{
			Contract.Invariant( _stream != null );
			Contract.Invariant( _buffer != null );
			Contract.Invariant( _buffer.Length == 16 );
		}

		public byte ReadByte()
		{
			int data = _stream.ReadByte();
			if( data == -1 )
				throw new EndOfStreamException();

			return (byte)data;
		}

		public sbyte ReadSByte()
		{
			int data = _stream.ReadByte();
			if( data == -1 )
				throw new EndOfStreamException();

			return (sbyte)(byte)( data & 0xff );
		}

		public short ReadInt16()
		{
			int read = _stream.Read( _buffer, 0, 2 );
			if( read != 2 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 2 );

			return BitConverter.ToInt16( _buffer, 0 );
		}

		public ushort ReadUInt16()
		{
			int read = _stream.Read( _buffer, 0, 2 );
			if( read != 2 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 2 );

			return BitConverter.ToUInt16( _buffer, 0 );
		}

		public int ReadInt24()
		{
			_buffer[0] = 0x00;
			_buffer[4] = 0x00;

			int read = _stream.Read( _buffer, 1, 3 );
			if( read != 3 )
				throw new EndOfStreamException();

			// if MSB is set, extend with 0xff to keep correct sign
			if( ( _buffer[1] & 0x80 ) != 0 )
			{
				_buffer[0] = 0xff;
				_buffer[4] = 0xff;
			}

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 1, 3 );

			return BitConverter.ToInt32( _buffer, 1 );
		}

		public uint ReadUInt24()
		{
			_buffer[0] = 0x00;
			_buffer[4] = 0x00;

			int read = _stream.Read( _buffer, 1, 3 );
			if( read != 3 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 1, 3 );

			return BitConverter.ToUInt32( _buffer, 1 );
		}

		public int ReadInt32()
		{
			int read = _stream.Read( _buffer, 0, 4 );
			if( read != 4 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 4 );

			return BitConverter.ToInt32( _buffer, 0 );
		}

		public uint ReadUInt32()
		{
			int read = _stream.Read( _buffer, 0, 4 );
			if( read != 4 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 4 );

			return BitConverter.ToUInt32( _buffer, 0 );
		}

		public long ReadInt64()
		{
			int read = _stream.Read( _buffer, 0, 8 );
			if( read != 8 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 8 );

			return BitConverter.ToInt64( _buffer, 0 );
		}

		public ulong ReadUInt64()
		{
			int read = _stream.Read( _buffer, 0, 8 );
			if( read != 8 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 8 );

			return BitConverter.ToUInt64( _buffer, 0 );
		}

		public float ReadSingle()
		{
			int read = _stream.Read( _buffer, 0, 4 );
			if( read != 4 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 4 );

			return BitConverter.ToSingle( _buffer, 0 );
		}

		public double ReadDouble()
		{
			int read = _stream.Read( _buffer, 0, 8 );
			if( read != 8 )
				throw new EndOfStreamException();

			// reverse if system is little endian, because big endian is supplied by Nbt format
			if( BitConverter.IsLittleEndian )
				Array.Reverse( _buffer, 0, 8 );

			return BitConverter.ToDouble( _buffer, 0 );
		}

		public byte[] ReadBytes( int count )
		{
			Contract.Requires( count >= 0 );
			Contract.Ensures( Contract.Result<byte[]>() != null );
			Contract.Ensures( Contract.Result<byte[]>().Length == count );

			var buffer = new byte[count];

			if( count > 0 )
			{
				int read = _stream.Read( buffer, 0, count );
				if( read != count )
					throw new EndOfStreamException();
			}

			return buffer;
		}

		public sbyte[] ReadSBytes( int count )
		{
			Contract.Requires( count >= 0 );
			Contract.Ensures( Contract.Result<sbyte[]>() != null );
			Contract.Ensures( Contract.Result<sbyte[]>().Length == count );

			var buffer = new sbyte[count];
			var tempBuffer = new byte[count];

			if( count > 0 )
			{
				int read = _stream.Read( tempBuffer, 0, count );
				if( read != count )
					throw new EndOfStreamException();

				// copy over, so bytes become signed bytes
				Buffer.BlockCopy( tempBuffer, 0, buffer, 0, read );
			}

			return buffer;
		}
	}
}